﻿namespace BrowserCacheBustingUtilities
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Text;

    internal static class FileManager
    {

        private static IOutput _output = new ConsoleOutput();

        private static string MD5HashFormatString
        {
            get
            {
                return MD5HashSuffix + "{0}{1}";
            }
        }

        private static string MD5HashSuffix
        {
            get
            {
                string hashSuffix = CacheBuster.MD5HashPrefix;

                if (string.IsNullOrEmpty(hashSuffix))
                    hashSuffix = "_";

                return hashSuffix;
            }
        }

        static FileManager()
        {
            OutputExclusionsToConsole = false;
        }

        internal static bool OutputExclusionsToConsole { get; set; }

        public static IOutput Output
        {
            get
            {
                return _output;
            }
            set
            {
                _output = value;
            }
        }

        internal static List<FileInfo> GetFiles(FileSearchData fileSearchData)
        {
            List<string> skippedFiles = new List<string>();
            List<FileInfo> processedFiles = new List<FileInfo>();

            List<DirectoryInfo> validFolderPaths = new List<DirectoryInfo>();

            foreach (DirectoryInfo folder in fileSearchData.FolderPaths.Select(c => new DirectoryInfo(c)))
            {
                if (fileSearchData.ExcludedFolderPaths.Any(c => GetDirectoryPath(folder.FullName).StartsWith(c, StringComparison.CurrentCultureIgnoreCase)) ||
                    (fileSearchData.FolderNamesFilter.Count > 0 && fileSearchData.FolderNamesFilterType == CacheBuster.Filter.Exclude && fileSearchData.FolderNamesFilter.Any(c => GetDirectoryPath(folder.FullName).IndexOf(@"\" + c + @"\", StringComparison.CurrentCultureIgnoreCase) != -1)))
                {
                    if (OutputExclusionsToConsole)
                        Output.WriteLine(String.Format("Skipping excluded folder '{0}'.", folder.FullName));

                    continue;
                }

                validFolderPaths.Add(folder);

                foreach (DirectoryInfo childFolder in folder.GetDirectories("*.*", SearchOption.AllDirectories))
                {
                    if (fileSearchData.ExcludedFolderPaths.Any(c => GetDirectoryPath(childFolder.FullName).StartsWith(c, StringComparison.CurrentCultureIgnoreCase)) ||
                        (fileSearchData.FolderNamesFilter.Count > 0 && fileSearchData.FolderNamesFilterType == CacheBuster.Filter.Exclude && fileSearchData.FolderNamesFilter.Any(c => GetDirectoryPath(childFolder.FullName).IndexOf(@"\" + c + @"\", StringComparison.CurrentCultureIgnoreCase) != -1)))
                    {
                        if (OutputExclusionsToConsole)
                            Output.WriteLine(String.Format("Skipping excluded folder '{0}'.", childFolder.FullName));

                        continue;
                    }

                    validFolderPaths.Add(childFolder);
                }
            }

            if (fileSearchData.FolderNamesFilter.Count > 0 && fileSearchData.FolderNamesFilterType == CacheBuster.Filter.Include)
            {
                List<DirectoryInfo> foldersToRemove = new List<DirectoryInfo>();

                foreach (DirectoryInfo directory in validFolderPaths)
                {
                    if (!fileSearchData.FolderNamesFilter.Any(c => string.Equals(c, directory.Name, StringComparison.CurrentCultureIgnoreCase)))
                        foldersToRemove.Add(directory);
                }

                foreach (DirectoryInfo directory in foldersToRemove)
                    validFolderPaths.Remove(directory);
            }

            RemoveRedundantFolderPaths(validFolderPaths);

            foreach (DirectoryInfo folder in validFolderPaths)
            {
                foreach (FileInfo file in folder.GetFiles("*.*", SearchOption.AllDirectories))
                {
                    if (skippedFiles.Any(c => String.Equals(file.FullName, c, StringComparison.CurrentCultureIgnoreCase)))
                        continue;

                    if (processedFiles.Any(c => String.Equals(file.FullName, c.FullName, StringComparison.CurrentCultureIgnoreCase)))
                        continue;

                    int startIndex = 0;

                    foreach (string folderPath in fileSearchData.FolderPaths)
                    {
                        if (file.FullName.Contains(folderPath) && startIndex < folderPath.Length)
                            startIndex = folderPath.Length - 1;
                    }

                    // check folder paths
                    if (fileSearchData.ExcludedFolderPaths.Any(c => file.FullName.StartsWith(c, StringComparison.CurrentCultureIgnoreCase))
                        || (fileSearchData.FolderNamesFilterType == CacheBuster.Filter.Exclude && fileSearchData.FolderNamesFilter.Count > 0 && fileSearchData.FolderNamesFilter.Any(c => file.FullName.Substring(startIndex).IndexOf(@"\" + c + @"\", StringComparison.CurrentCultureIgnoreCase) != -1)))
                    {
                        if (OutputExclusionsToConsole)
                            Output.WriteLine(String.Format("Skipping file '{0}' because parent directory heirarchy is excluded.", file.FullName));

                        skippedFiles.Add(file.FullName);
                        continue;
                    }

                    if (fileSearchData.FolderNamesFilterType == CacheBuster.Filter.Include && fileSearchData.FolderNamesFilter.Count > 0 && !fileSearchData.FolderNamesFilter.Any(c => string.Equals(c, file.Directory.Name, StringComparison.CurrentCultureIgnoreCase)))
                    {
                        if (OutputExclusionsToConsole)
                            Output.WriteLine(String.Format("Skipping file '{0}' because parent directory name is not included.", file.FullName));

                        skippedFiles.Add(file.FullName);
                        continue;
                    }

                    // check file paths
                    if (fileSearchData.FilePathsFilterType == CacheBuster.Filter.Include && fileSearchData.FilePathsFilter.Count > 0)
                    {
                        if (!fileSearchData.FilePathsFilter.Any(c => String.Equals(file.FullName, c, StringComparison.CurrentCultureIgnoreCase)))
                        {
                            if (OutputExclusionsToConsole)
                                Output.WriteLine(String.Format("Skipping non included file '{0}'.", file.FullName));

                            skippedFiles.Add(file.FullName);
                            continue;
                        }
                    }
                    else if (fileSearchData.FilePathsFilterType == CacheBuster.Filter.Exclude && fileSearchData.FilePathsFilter.Count > 0)
                    {
                        if (fileSearchData.FilePathsFilter.Any(c => String.Equals(file.FullName, c, StringComparison.CurrentCultureIgnoreCase)))
                        {
                            if (OutputExclusionsToConsole)
                                Output.WriteLine(String.Format("Skipping excluded file '{0}'.", file.FullName));

                            skippedFiles.Add(file.FullName);
                            continue;
                        }
                    }

                    // check file names
                    if (fileSearchData.FileNamesFilterType == CacheBuster.Filter.Include && fileSearchData.FileNamesFilter.Count > 0)
                    {
                        if (!fileSearchData.FileNamesFilter.Any(c => String.Equals(file.Name, c, StringComparison.CurrentCultureIgnoreCase)))
                        {
                            if (OutputExclusionsToConsole)
                                Output.WriteLine(String.Format("Skipping non included file '{0}'.", file.FullName));

                            skippedFiles.Add(file.FullName);
                            continue;
                        }
                    }
                    else if (fileSearchData.FileNamesFilterType == CacheBuster.Filter.Exclude && fileSearchData.FileNamesFilter.Count > 0)
                    {
                        if (fileSearchData.FileNamesFilter.Any(c => String.Equals(file.Name, c, StringComparison.CurrentCultureIgnoreCase)))
                        {
                            if (OutputExclusionsToConsole)
                                Output.WriteLine(String.Format("Skipping excluded file '{0}'.", file.FullName));

                            skippedFiles.Add(file.FullName);
                            continue;
                        }
                    }

                    // check extensions
                    string extension = file.Extension.ToLower();

                    if (fileSearchData.FileExtensionsFilterType == CacheBuster.Filter.Include && fileSearchData.FileExtensionsFilter.Count > 0)
                    {
                        if (!fileSearchData.FileExtensionsFilter.Any(c => String.Equals(extension, c, StringComparison.CurrentCultureIgnoreCase)))
                        {
                            if (OutputExclusionsToConsole)
                                Output.WriteLine(String.Format("Skipping file '{0}' because the extension '{1}' has not been included.", file.FullName, extension));

                            skippedFiles.Add(file.FullName);
                            continue;
                        }
                    }
                    else if (fileSearchData.FileExtensionsFilterType == CacheBuster.Filter.Exclude && fileSearchData.FileExtensionsFilter.Count > 0)
                    {
                        if (fileSearchData.FileExtensionsFilter.Any(c => String.Equals(extension, c, StringComparison.CurrentCultureIgnoreCase)))
                        {
                            if (OutputExclusionsToConsole)
                                Output.WriteLine(String.Format("Skipping file '{0}' because the extension '{1}' has been excluded.", file.FullName, extension));

                            skippedFiles.Add(file.FullName);
                            continue;
                        }
                    }

                    processedFiles.Add(new FileInfo(file.FullName));
                }
            }

            return processedFiles;
        }

        private static void RemoveRedundantFolderPaths(List<DirectoryInfo> validFolderPaths)
        {
            List<DirectoryInfo> directoriesToRemove = new List<DirectoryInfo>();

            foreach (DirectoryInfo directory in validFolderPaths)
            {
                if (validFolderPaths.Any(c => !string.Equals(GetDirectoryPath(c.FullName), GetDirectoryPath(directory.FullName), StringComparison.CurrentCultureIgnoreCase)
                    && string.Equals(GetDirectoryPath(directory.Parent.FullName), GetDirectoryPath(c.FullName), StringComparison.CurrentCultureIgnoreCase)))
                {
                    directoriesToRemove.Add(directory);
                }
            }

            foreach (DirectoryInfo directory in directoriesToRemove)
                validFolderPaths.Remove(directory);
        }

        internal static List<Asset> FindFiles(FileSearchData fileSearchData, bool computeHash = false)
        {
            List<Asset> assets = new List<Asset>();
            var files = GetFiles(fileSearchData);
            Output.WriteLine(string.Format(" {0} files have been found.", files.Count));

            foreach (FileInfo file in files)
            {
                string originalFilePath = file.FullName;
                string originalHash = GetFileHash(originalFilePath);

                string newHash = computeHash ? ComputeMd5Hash(originalFilePath) : String.Empty;
                string newFilePath = computeHash ? GetNewFileName(originalFilePath, newHash) : String.Empty;

                if (CacheBuster.CacheBustingType == CacheBuster.CacheBustType.Version && computeHash)
                {
                    string filePath = VersionManager.GetFilePathWithoutVersionNumber(originalFilePath);
                    int? versionNumber = VersionManager.GetFileVersionNumber(originalFilePath);
                    FileVersion fileVersion = VersionManager.FindFileVersion(filePath);

                    if (fileVersion != null)
                    {
                        versionNumber = fileVersion.VersionNumber;
                        originalHash = fileVersion.Hash;
                    }
                    else
                    {
                        fileVersion = new FileVersion();
                        fileVersion.FilePath = filePath;
                        fileVersion.Hash = newHash;
                    }

                    if (!versionNumber.HasValue)
                        versionNumber = 1;
                    else
                    {
                        if (!string.Equals(originalHash, newHash))
                            versionNumber = versionNumber.Value + 1;
                    }

                    fileVersion.VersionNumber = versionNumber.Value;
                    fileVersion.Hash = newHash;
                    VersionManager.UpdateFileVersion(fileVersion);
                    newFilePath = VersionManager.GetFilePathWithVersionNumber(filePath, fileVersion.VersionNumber);
                }

                Asset asset = new Asset
                {
                    OriginalFilePath = originalFilePath,
                    OriginalHash = originalHash,
                    NewFilePath = newFilePath,
                    NewHash = newHash,
                    DirectoryPath = originalFilePath.Substring(0, originalFilePath.LastIndexOf(@"\") + 1)
                };

                assets.Add(asset);
            }

            return assets;
        }

        internal static Dictionary<string, int> FindUniqueFileExtensions(FileSearchData fileSearchData, string message)
        {
            Dictionary<string, int> extensions = new Dictionary<string, int>();
            List<FileInfo> files = GetFiles(fileSearchData);

            foreach (var file in files)
            {
                string extension = file.Extension.ToLower();

                if (extensions.ContainsKey(extension))
                    extensions[extension] = extensions[extension] + 1;
                else
                    extensions.Add(extension, 1);
            }

            Output.WriteLine();
            Output.WriteLine(message);

            foreach (var entry in (from entry in extensions orderby entry.Value descending select entry))
                Output.WriteLine(String.Format("{0} : {1}", entry.Key, entry.Value));

            Output.WriteLine();

            return extensions;
        }

        internal static string ComputeMd5Hash(string filePath)
        {
            Output.Write(string.Format("Computing MD5 hash for file '{0}'.", filePath));
            StringBuilder sb = new StringBuilder();

            Stopwatch stopwatch = Stopwatch.StartNew();

            using (MD5 md5Hasher = MD5.Create())
            {
                using (FileStream fs = File.OpenRead(filePath))
                {
                    foreach (Byte b in md5Hasher.ComputeHash(fs))
                        sb.Append(b.ToString("x2").ToLower());
                }
            }

            string hash = sb.ToString();
            Output.WriteLine(string.Format(" Hash: {0}. Time taken in seconds: {1}.", hash, stopwatch.Elapsed.TotalSeconds.ToString("N2")));

            return hash;
        }

        internal static string GetFileHash(string filePath)
        {
            FileInfo fileInfo = new FileInfo(filePath);
            string fileName = fileInfo.Name;

            if (fileName.Contains(MD5HashSuffix))
            {
                fileName = fileName.Substring(fileName.LastIndexOf(MD5HashSuffix) + MD5HashSuffix.Length);
                return fileName.Contains(".") ? fileName.Substring(0, fileName.IndexOf(".")) : fileName;
            }

            return String.Empty;
        }

        internal static string GetNewFileName(string filePath, string hash)
        {
            FileInfo fileInfo = new FileInfo(filePath);
            string extension = fileInfo.Extension;
            string fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - extension.Length);

            if (fileName.Contains(MD5HashSuffix))
                fileName = fileName.Substring(0, fileName.LastIndexOf(MD5HashSuffix));

            fileName = fileName + String.Format(MD5HashFormatString, hash, extension);

            string path = fileInfo.Directory.FullName;

            if (!path.EndsWith(@"\"))
                path += @"\";

            return path + fileName;
        }

        internal static string GetFileNameWithoutHash(string filePath)
        {
            FileInfo fileInfo = new FileInfo(filePath);
            string extension = fileInfo.Extension;
            string fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - extension.Length);

            if (fileName.Contains(MD5HashSuffix))
                fileName = fileName.Substring(0, fileName.LastIndexOf(MD5HashSuffix));

            fileName = fileName + extension;

            string path = fileInfo.Directory.FullName;

            if (!path.EndsWith(@"\"))
                path += @"\";

            return path + fileName;
        }

        internal static string GetDirectoryPath(string path)
        {
            if (!path.EndsWith(@"\"))
                path += @"\";

            return path;
        }
    }
}